package weeny.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import prefuse.data.parser.CollectionParser;


/**
 * A counter utility, keeping a Key->Number (Integer/Double) map.
 * 
 * There is a flag indicating that do we allow netagive values? If we do not
 * allow and a value is about to be changed to negative, an exception will be
 * thrown out.
 * 
 * If a key's value is changed to zero, it will be removed from the counter.
 * Just looks like it has never been added before.
 * 
 * Whey a value is a Integer but you want to add a double value to it, the value
 * will be changed to converted to double.
 * 
 * @author weeny
 */
public class Counter<T>{
	//actual data
	protected Map<T, Number> cnt = new HashMap<T, Number>();
	//do we allow negative number value?
	protected boolean supportNegative = false;
	/**
	 * Default constructor, we do not allow values smaller than zero
	 */
	public Counter(){
		this(false);
	}
	public Counter(boolean supportNegative){
		this.supportNegative = supportNegative;
	}
	/**
	 * Initial the values in this counter. Every element in this collection
	 * should be an Entry<?, Number>, otherwise, you will get a nice exception. 
	 */
	public Counter(Collection<Entry> coll){
		for (Entry<? extends T, Number> entry : (Collection<Entry>) coll) {
			cnt.put(entry.getKey(), entry.getValue());
		}
	}
	/**
	 * Get the size of the counter, i.e., the key size
	 */
	public int size(){
		return cnt.size();
	}
	/**
	 * Increase the key by one.
	 */
	public void increase(T key){
		decrease(key, -1);
	}
	/**
	 * Increase the key by a specific value.
	 */
	public void increase(T key, Number val){
		if (val instanceof Integer) {
			decrease(key, 0-val.intValue());
		}
		else{
			decrease(key, 0-val.doubleValue());
		}
	}
	/**
	 * Decrease the key by one.
	 */
	public void decrease(T key){
		decrease(key, 1);
	}
	/**
	 * Decrease the key by a specific value.
	 */
	public void decrease(T key, Number val){
		Number newVal = 0;
		Number current = cnt.containsKey(key)? cnt.get(key) : 0;
		boolean useInteger = current instanceof Integer;
		useInteger = useInteger && val instanceof Integer; 
		if (useInteger){
			newVal = current.intValue()-val.intValue();
		}
		else {
			newVal = current.doubleValue()-val.doubleValue();
		}

		if (newVal.doubleValue() == 0) {
			cnt.remove(key);
			return;
		}
		else if (supportNegative || newVal.doubleValue() > 0) {
			cnt.put(key, newVal);
		}
		else if (newVal.doubleValue() < 0) {
			throw new IllegalArgumentException("cannot be negative: "+key);
		}
		else{
			throw new IllegalArgumentException("impossible to reach here");
		}
	}
	public boolean contains(T key){
		return cnt.containsKey(key);
	}
	public Number safeCount(T key){
		Number result = getCount(key);
		return result == null ? 0 : result;
	}
	public Number getCount(T key){
		return cnt.get(key);
	}
	public Set<T> keys(){
		return cnt.keySet();
	}

	/**
	 * given a mapper from old keys to new keys, if any.
	 * union the counter of both keys and set to the new keys.
	 * 
	 * Useful when you need to merge some keys in the counter
	 * 
	 * @param map the map mapping from old keys to new keys
	 * @return a new counter instance
	 */
	public Counter<T> renameKeys(Map<T, T> map){
		Counter<T> result = new Counter<T>(this.supportNegative);
		for(Entry<T, Number> number : cnt.entrySet()){
			T key = number.getKey();
			if (map.containsKey(key)) {
				key = map.get(key);
			}
			result.increase(key, this.getCount(number.getKey()));
		}
		return result;
	}
	
	/**
	 * Merge another counter into this counter
	 */
	public void mergeEqual(Counter<T> another){
		for(Entry<T, Number> entry : another.cnt.entrySet()){
			this.increase(entry.getKey(), entry.getValue());
		}
	}
	
	/**
	 * Removes from this counter all of its elements that are *NOT* contained in
	 * the passed-in collection. 
	 */
	public void retains(Collection coll){
		Counter<T> newCounter = new Counter<T>(coll);
		this.retains(newCounter);
	}
	
	/**
	 * Removes from this counter all of its elements that are *NOT* contained in
	 * the passed-in counter (We only consider the keys). 
	 */
	public void retains(Counter<T> another){
		Map<T, Number> newCnt = new HashMap<T, Number>();
		for(Entry<T, Number> entry : another.cnt.entrySet()){
			if (cnt.containsKey(entry.getKey())) {
				newCnt.put(entry.getKey(), this.cnt.get(entry.getKey()));
			}
		}
		cnt = newCnt;
	}
	
	/**
	 * Remove every key contained by the passed-in counter 
	 */
	public void removes(Counter<T> another){
		for(Entry<T, Number> entry : another.cnt.entrySet()){
			cnt.remove(entry.getKey());
		}
	}
	
	/**
	 * Find the top list, in which all elements are bigger or equal to the 
	 * passed-in parameter
	 */
	public List<Map.Entry<T, Number>> topByThreshold(int value){
		List<Map.Entry<T, Number>> list = topByCount(cnt.size());
		List<Map.Entry<T, Number>> result = new ArrayList();
		for(int i = 0; i<list.size(); i++){
			if (list.get(i).getValue().intValue() < value)
				break;
			result.add(list.get(i));
		}
		return result;
	}
	
	/**
	 * Find the top list, which has the size of num
	 */
	public List<Map.Entry<T, Number>> topByCount(int num){
		return sortEntry(num, DESCENDING);
	}
	
	/**
	 * Find the buttom list, which has the size of num
	 */
	public List<Map.Entry<T, Number>> buttomByCount(int num){
		return sortEntry(num, ASCENDING);
	}
	/**
	 * Find the top list (only keys), which has the size of num
	 */
	public List<T> topKeysByCount(int num) {
		return sortKey(num, DESCENDING);
	}
	/**
	 * Find the buttom list (only keys), which has the size of num		
	 */
	public List<T> buttomKeysByCount(int num) {
		return sortKey(num, ASCENDING);
	}
	
	/**
	 * Find the first num keys by using the comparator comp
	 */
	public List<T> sortKey(int num, Comparator<Map.Entry<T, Number>> comp){
		List<Map.Entry<T, Number>> list = sortEntry(num, comp);
		List<T> result = new ArrayList<T>();
		for(Map.Entry<T, Number> entry : list){
			result.add(entry.getKey());
		}
		return result;
	}
	/**
	 * Sort every entry accendingly
	 */
	public List<Map.Entry<T, Number>> ascending(){
		return buttomByCount(cnt.size());
	}
	/**
	 * Sort every entry decendingly
	 */
	public List<Map.Entry<T, Number>> descending(){
		return topByCount(cnt.size());
	}
	/**
	 * Find the first num entries by using the comparator comp
	 */
	public List<Map.Entry<T, Number>> sortEntry(int num, Comparator comp){
		List<Map.Entry<T, Number>> list = new ArrayList(cnt.entrySet());
		Collections.sort(list, comp);
		if (list.size() <= num) {
			return list;
		}
		List<Map.Entry<T, Number>> result = new ArrayList();
		for(int i = 0; i<num; i++){
			result.add(list.get(i));
		}
		return result;
	}
	
	//Ascending comparator, which considers every key as a double value
	private Comparator ASCENDING = new Comparator<Map.Entry<T, Number>>() {
		public int compare(Map.Entry<T, Number> o1, Map.Entry<T, Number> o2) {
			if (o1.getValue().doubleValue() < o2.getValue().doubleValue())
				return -1;
			else if (o1.getValue().doubleValue() > o2.getValue().doubleValue())
				return 1;
			else return 0;
		}
	};
	//Descending comparator, which considers every key as a double value
	private Comparator DESCENDING = new Comparator<Map.Entry<T, Number>>() {
		public int compare(Map.Entry<T, Number> o1, Map.Entry<T, Number> o2) {
			return 0-ASCENDING.compare(o1, o2);
		}
	};
	/**
	 * Converting the counter to a string. Can be used in toString() function or
	 * parser function.
	 */
	public String encode() {
		String[] strings = new String[cnt.size()*2+1];
		int index = 0;
		strings[0] = this.supportNegative+"";
		for(Entry entry : cnt.entrySet()){
			strings[1+index*2+0] = entry.getKey().toString();
			strings[1+index*2+1] = entry.getValue().toString();
			index++;
		}
		return CollectionParser.string2Csv(strings);
	}
}